package abacab.ext;

import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Map.Entry;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import abacab.ActorSystem;
import abacab.serial.Serializer;
import abacab.serial.SerializerFactory;

import com.typesafe.config.Config;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigException.BadValue;
import com.typesafe.config.ConfigException.Missing;
import com.typesafe.config.ConfigValue;

/**
 * The extension for remoting
 */
public class SerializationExtension implements Extension {
	/** The logger */
	private static final Logger							logger									= LoggerFactory
																																			.getLogger(SerializationExtension.class);

	/** The serialization configuration key */
	private static final String							SERIALIZATION_CONFIG		= "abacab.serialization";

	/** The serialization factories configuration key */
	private static final String							SERIALIZATION_FACTORIES	= "factories";

	/** The classes mapping configuration key */
	private static final String							CLASS_MAPPING						= "class-mapping";

	/** The name for the default serializer */
	private static final String							DEFAULT_SERIALIZER			= "default";

	/** The serializers */
	private final Map<String, Serializer>		serializers;

	/** The serializers by id. */
	private final Map<Integer, Serializer>	serializersById;

	/**  */
	private final Map<Class, Serializer>		serializersByClass;

	/** The default serializer */
	private Serializer											defaultSerializer;

	/**
	 * Create the extension
	 * 
	 * @param actorSystem
	 *          the actor system
	 * @throws ConfigException
	 *           if the configuration is invalid
	 */
	public SerializationExtension(ActorSystem actorSystem) throws ConfigException {
		Config serializationConfig = actorSystem.getConfig().getConfig(SERIALIZATION_CONFIG);

		// Get the factory's implementation
		this.serializers = new HashMap<String, Serializer>();
		this.serializersById = new HashMap<Integer, Serializer>();
		Config serialFactories = serializationConfig.getConfig(SERIALIZATION_FACTORIES);
		for (Entry<String, ConfigValue> entry : serialFactories.entrySet()) {
			try {
				registerSerializer(serializationConfig, entry.getKey(), (String) entry.getValue().unwrapped());
			}
			catch (ClassCastException e) {
				throw new BadValue(SERIALIZATION_FACTORIES,
						"The serialization factories should map keys to the factories class names: " + entry.getKey() + " = "
								+ entry.getValue().unwrapped());
			}
		}
		if (this.serializers.isEmpty()) {
			throw new Missing(SERIALIZATION_CONFIG + "." + SERIALIZATION_FACTORIES, new IllegalStateException(
					"No factory for serialization"));
		}
		if (this.defaultSerializer == null) {
			this.defaultSerializer = this.serializers.values().iterator().next();
			logger.warn("No default serializer was defined: using \"{}\" as default", this.defaultSerializer.getClass()
					.getName());
		}

		// Load the class mappings
		this.serializersByClass = new IdentityHashMap<Class, Serializer>();
		Config classMapping = serializationConfig.getConfig(CLASS_MAPPING);
		for (Entry<String, ConfigValue> entry : classMapping.entrySet()) {
			Class clazz;
			try {
				clazz = Class.forName(entry.getKey());
			}
			catch (ClassNotFoundException e) {
				logger.warn("The class \"{}\" found in the serialization mapping does not exist", entry.getKey());
				continue;
			}
			String serializerName = entry.getValue().unwrapped().toString();
			Serializer serializer = this.serializers.get(serializerName);
			if (serializer == null) {
				serializer = this.defaultSerializer;
			}
			this.serializersByClass.put(clazz, serializer);
		}
	}

	/**
	 * Register the serializer
	 * 
	 * @param config
	 *          the configuration
	 * @param name
	 *          the name
	 * @param serialFactory
	 *          the factory for the serializer
	 * @throws ConfigException
	 *           if there is an error in the configuration
	 */
	@SuppressWarnings("unchecked")
	private final void registerSerializer(Config config, String name, String serialFactory) throws ConfigException {
		Class<SerializerFactory> factoryClass;
		try {
			factoryClass = (Class<SerializerFactory>) Class.forName(serialFactory);
		}
		catch (ClassNotFoundException e) {
			throw new BadValue(SERIALIZATION_FACTORIES, "The serializer's factory for \"" + name + "\" (" + serialFactory
					+ ") does not exist", e);
		}
		catch (ClassCastException e) {
			throw new BadValue(SERIALIZATION_FACTORIES, "The serializer's factory for \"" + name + "\" (" + serialFactory
					+ ") does not implement \"" + SerializerFactory.class.getName() + "\"");
		}

		// Build the factory
		SerializerFactory factory;
		try {
			factory = factoryClass.newInstance();
		}
		catch (Exception e) {
			throw new BadValue(SERIALIZATION_FACTORIES, "The serializer's factory for \"" + name + "\" (" + serialFactory
					+ ") can not be built", e);
		}

		Serializer serializer = factory.buildSerializer(config);

		this.serializers.put(name, serializer);
		if (DEFAULT_SERIALIZER.equalsIgnoreCase(name)) {
			if (this.defaultSerializer == null) {
				this.defaultSerializer = serializer;
			}
			else {
				logger.warn("The default serializer is already defined \"{}\"", this.defaultSerializer.getClass().getName());
			}
		}
		Serializer other = this.serializersById
				.put(Integer.valueOf(serializer.getClass().getName().hashCode()), serializer);
		if (other != null) {
			throw new ConfigException.BadValue(SERIALIZATION_FACTORIES, //
					"The serializer \"" + serializer.getClass().getName() + "\" has the same hashcode than \""
							+ other.getClass().getName() + "\": cannot make a unique identifier");
		}
	}

	@Override
	public void dispose() {
		for (Serializer serializer : this.serializers.values()) {
			serializer.dispose();
		}
		this.serializers.clear();
	}

	/**
	 * @param serializer
	 *          the serializer
	 * @return the identifier
	 */
	public Integer getIdForSerializer(Serializer serializer) {
		return Integer.valueOf(serializer.getClass().getName().hashCode());
	}

	/**
	 * @param serializerId
	 *          the serializer's id.
	 * @return the serializer to use
	 */
	public Serializer getSerializerForId(Integer serializerId) {
		return this.serializersById.get(serializerId);
	}

	/**
	 * @param serializerName
	 *          the serializer's name
	 * @return the serializer to use
	 */
	public Serializer getSerializerByName(String serializerName) {
		Serializer serializer = this.serializers.get(serializerName);
		if (serializer != null) {
			return serializer;
		}
		logger.warn("Serializer \"{}\" is undefined: using default", serializerName);
		return this.defaultSerializer;
	}

	/**
	 * @param message
	 *          the message
	 * @return the serializer to use
	 */
	public Serializer getSerializerFor(Object message) {
		Serializer serializer = this.serializersByClass.get(message.getClass());
		if (serializer != null) {
			return serializer;
		}
		return this.defaultSerializer;
	}
}